查看原文
其他

干货 | Kotlin超棒的语言特性

何伦 携程技术中心 2019-05-02

作者简介

 

何伦,携程度假BU移动端资深研发经理,负责iOS、Android平台上跟团游产品预订流程的前端页面的研发工作。对新技术有着浓厚的兴趣。


自从2017年Google宣布Kotlin成为Android官方开发语言之后,Kotlin受到广大Android开发者的追捧。其强大的安全性,简洁性和与Java的互操作性,为开发者带来了耳目一新的开发体验,也极大提升了Android原生代码的开发效率。


不过大部分开发者对Kotlin的使用,仍然局限于把Java代码逻辑按照Kotlin语法进行转换的层面,其实Kotlin和Java虽然具有很强的互操作性,但本质上还是两种完全不同设计思想的语言。


本文在假定读者有一定Kotlin开发基础的前提下,详细讲解一些具有Kotlin特色的实用的语言特性,帮助开发者能够写出更加“具有Kotlin风格”的代码。这些语言特性包括空安全、Elvis表达式、简洁字符串等等。


01

更加安全的指针操作


在Kotlin中,一切皆是对象。不存在int, double等关键字,只存在Int, Double等类。


所有的对象都通过一个指针所持有,而指针只有两种类型:var 表示指针可变,val表示指针不可变。为了获得更好的空安全,Kotlin中所有的对象都明确指明可空或者非空属性,即这个对象是否可能为null。

对于可空类型的对象,直接调用其方法,在编译阶段就会报错。这样就杜绝了空指针异常NullPointerException的可能性。

如上图,编译器会报错

Error:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String


02

?表达式和Elvis表达式


Kotlin特有的?表达式和Elvis表达式可以在确保安全的情况下,写出更加简洁的代码。比如我们在Android页面开发中常见的删除子控件操作,用Java来写是这样的:

为了获得更加安全的代码,我们不得不加上很多if else 判断语句,来确保不会产生空指针异常。但Kotlin的?操作符可以非常简洁地实现上述逻辑:

那么这个?表达式的内在逻辑是什么呢?以上述代码为例,若view == null,则后续调用均不会走到,整个表达式直接返回null,也不会抛出异常。也就是说,?表达式中,只要某个操作对象为null,则整个表达式直接返回null。


除了?表达式,Kotlin还有个大杀器叫Elvis表达式,即?: 表达式,这两个表达式加在一起可以以超简洁的形式表述一个复杂逻辑。

以上面表达式为例,我们以红线把它划分成两个部分。若前面部分为null,则整个表达式返回值等于c的值,否则等于前面部分的值。把它翻译成Java代码,是这样的

同样等同于这样

即Elvis表达式的含义在于为整个 ?表达式托底,即若整个表达式已经为null的情况下,Elvis表达式能够让这个表达式有个自定义的默认值。这样进一步保证了空安全,同时代码也不失简洁性。


03

更简洁的字符串


同Java一样,Kotlin也可以用字面量对字符串对象进行初始化,但Kotlin有个特别的地方是使用了三引号”””来方便长篇字符串的书写。而且这种方法还不需要使用转义符。做到了字符串的所见即所得。

同时,Kotlin还引入了字符串模板,可以在字符串中直接访问变量和使用表达式:


04

强大的when语句


Kotlin中没有switch操作符,而是使用when语句来替代。同样的,when 将它的参数和所有的分支条件顺序比较,直到某个分支满足条件。如果其他分支都不满足条件将会进入 else 分支。

但功能上when语句要强大得多。首先第一点是,我们可以用任意表达式(而不只是常量)作为分支条件,这点switch就做不到。如下述代码,前面三个分支条件分别是:1、变量在[1, 10]区间内, 2、变量x不在[10, 20]区间内,3、变量x是一个字符串。这个表达式用switch语句基本无法实现,只能用if else 链来实现。

说起if else 链,我们可以直接用when语句把它给替换掉:


05


对象比较


Java的 == 操作符是比较引用值,但Kotlin 的 == 操作符是比较内容, === 才是比较引用值。基于这点,我们可以写出逻辑更简洁合理的代码:

上述代码可以直接用when语句实现


06

Nullable Receiver


NullableReceiver我将其翻译成“可空接收者”,要理解接收者这个概念,我们先了解一下Kotlin中一个重要特性:扩展。Kotlin能够扩展一个类的新功能,这个扩展是无痕的,即我们无需继承该类或使用像装饰者的设计模式,同时这个扩展对使用者来说也是透明的,即使用者在使用该类扩展功能时,就像使用这个类自身的功能一样的。


声明一个扩展函数,我们需要用一个接收者类型,也就是被扩展的类型来作为他的前缀,以下述代码为例:

上述代码为 MutableList<Int> 添加一个swap 函数, 我们可以对任意 MutableList<Int> 调用该函数了:

其中MutableList<Int>就是这个扩展函数的接收者。值得注意的是,Kotlin允许这个接收者为null,这样我们可以写出一些在Java里面看似不可思议的代码。比如我们要把一个对象转换成字符串,在Kotlin中可以直接这么写:

上述代码先定义了一个空指针对象,然后调用toString方法,会不会Crash?其实不会发生Crash,答案就在“可空接收者”,也就是Nullable Receiver,我们可以看下这个扩展函数的定义:

扩展函数是可以拿到接收者对象的指针的,即this指针。从这个方法的定义我们可以看到,这个方法是对Any类进行扩展,而接收者类型后面加了个?号,所以准确来说,是对Any?类进行扩展。我们看到,扩展函数一开始就对接收者进行判空,若为null,则直接返回 “null” 字符串。所以无论对于什么对象,调用toString方法不会发生Crash.


07

关键字object


前面说过,Kotlin中一切皆为对象,object在Kotlin中是一个关键字,笼统来说是代表“对象”,在不同场景中有不同用法。


第一个是对象表达式,可以直接创建一个继承自某个(或某些)类型的匿名类的对象,而无须先创建这个对象的类。这一点跟Java是类似的:

第二,对象字面量。这个特性将数字字面量,字符串字面量扩展到一般性对象中了。对应的场景是如果我们只需要“一个对象而已”,并不需要特殊超类型。典型的场景是在某些地方,比如函数内部,我们需要零碎地使用一些一次性的对象时,非常有用。

第三,对象声明。这个特性类似于Java中的单例模式,但我们不需要写单例模式的样板代码即可以实现。

请注意上述代码是声明了一个对象,而不是类,而我们想要使用这个对象,直接引用其名称即可:


08

有趣的冒号


从语法上来看,Kotlin大量使用了冒号(:)这一符号,我们可以总结一下,这个冒号在Kotlin中究竟代表什么。


考虑下面四种场景:


  • 在变量定义中,代表变量的类型

  • 在类定义中,代表基类的类型

  • 在函数定义中,代表函数返回值的类型

  • 在匿名对象中,代表对象的类型


笼统来说,Kotlin的设计者应该就是想用冒号来笼统表示类型这一概念。


09

可观察属性


可观察属性,本质就是观察者模式,在Java中也可以实现这个设计模式,但Kotlin实现观察者模式不需要样板代码。在谈Kotlin的可观察属性前,先看下Kotlin里面的委托。同样的,委托也是一种设计模式,它的结构如下图所示:

Kotlin在语言级别支持它,不需要任何样板代码。Kotlin可以使用by关键字把子类的所有公有成员都委托给指定对象来实现基类的接口:

上述代码中,Base是一个接口,BaseImpl是它的一个实现类,通过by b语句就可以把Derived类中的所有公有成员全部委托给b对象来实现。我们在创建Derived类时,在构造器中直接传入一个BaseImpl的实例,那么调用Derived的方法等同于调用BaseImpl的实例的方法,访问Derived的属性也等同于访问BaseImpl的实例的属性。


回到可观察属性这个概念,Kotlin通过 Delegates.observable()实现可观察属性:

上述代码中,name是一个属性,改变它的值都会自动回调{ kProperty,  oldName,  newName -> }这个lambda表达式。简单来说,我们可以监听name这个属性的变化。


可观察属性有什么用处呢?ListView中有一个经典的Crash:在数据长度与Adapter中的Cell的长度不一致时,会报IllegalStateException异常。这个异常的根本原因是修改了数据之后,没有调用notifyDataSetChanged,导致ListView没有及时刷新。如果我们把数据做成可观察属性,在观察回调方法中直接刷新ListView,可以杜绝这个问题。


10

函数类型


Kotlin中一切皆是对象,函数也不例外。在Kotlin中,函数本身也是对象,可以拥有类型并实例化。Kotlin 使用类似 (Int) -> String 的一系列函数类型来处理函数的声明,比如我们常见的点击回调函数:

箭头表示法是右结合的,(Int) -> (Int) -> Unit 等价于(Int) ->((Int) -> Unit),但不等于 ((Int) -> (Int)) -> Unit。可以通过使用类型别名给函数类型起一个别称:

函数对象最大的作用是可以轻易地实现回调,而不需要像Java那样通过代理类才可以做到。我们以ScrollView滑动的回调为例,看一下使用Java编写一份Callback需要花费多大成本。对于主调方,即MyScrollView类而言,首先我们需要一个Callback的接口(OnScrollCallback),这个接口里面有一个待实现的onScroll方法。然后需要一个属性来保存回调对象。最后在View滑动的时候,我们调用这个回调对象的onScroll以实现回调。

对于被调方,即MyScrollView的使用者而言,我们需要一个实现OnScrollCallback接口的对象。然后设置成MyScrollView的回调对象,才能够实现滑动回调。

我们只是实现一个简单的回调而已,为什么还要这么复杂呢?本质上是因为Java里面函数并不是对象,所以要实现回调,必须要实现一个代理类来包装这个函数,否则我们无法传递这个函数给主调方。


Kotlin实现回调就是完全不一样的方式了,因为Kotlin的函数也是对象,所以我们直接把函数对象传递给主调方即可。

看一下上面的代码,就是这么简单!


再介绍下如何将函数类型实例化,有几种常见方式:


一是使用函数字面值的代码块,比如lambda 表达式   { a, b -> a + b },或者匿名函数fun(s: String): Int { return s.toIntOrNull()?: 0 }


二是使用已有声明的可调用引用,包括顶层、局部、成员、扩展函数 ::isOdd    String::toInt,或者顶层、成员、扩展属性 List<Int>::size,或者是构造函数 ::Regex


三是使用实现函数类型接口的自定义类的实例

四是编译器推断


11

工具


对于初学Kotlin的开发者而言,编译器提供了贴心的小工具,甚至可以直接把Java代码转换成Kotlin代码。直接把Java代码拷贝到.kt文件中,编译器会弹出如下提示:



Kotlin与Java是100%兼容的,因为它最终会编译成Java字节码,我们可以通过 Android Studio工具看到编译的bytecode:



我们还可以把编译出来的Java字节码反编译成Java代码,这样可以窥探Kotlin的实现机理:


事实上,Kotlin优秀的语言特性绝对不止本文提到的这几种,还有很多,比如函数默认参数、扩展属性、懒初始化、局部函数、数据类,等等。欢迎大家在学习的过程中一起交流。


【推荐阅读】



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存